// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package auth.plugins.flutter.io.fist_local_auth;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import androidx.annotation.RequiresApi;
import androidx.biometric.BiometricConstants;
import androidx.core.hardware.fingerprint.FingerprintManagerCompat;
import androidx.fragment.app.FragmentActivity;

import java.util.concurrent.Executor;

import javax.crypto.Cipher;

import io.flutter.plugin.common.MethodCall;

import static android.content.ContentValues.TAG;
import static auth.plugins.flutter.io.fist_local_auth.CipherHelper.DEFAULT_KEY_NAME;
import static auth.plugins.flutter.io.fist_local_auth.CipherHelper.HAS_FINGER_KEY;

/**
 * Authenticates the user with fingerprint and sends corresponding response back to Flutter.
 *
 * <p>One instance per call is generated to ensure readable separation of executable paths across
 * method calls.
 */
@SuppressWarnings("deprecation")
@RequiresApi(api = Build.VERSION_CODES.M)
class AuthenticationHelper extends BiometricPromptV2.AuthenticationCallback
        implements Application.ActivityLifecycleCallbacks {

    /**
     * The callback that handles the result of this authentication process.
     */
    interface AuthCompletionHandler {

        /**
         * Called when authentication was successful.
         */
        void onSuccess();

        /**
         * Called when authentication failed due to user. For instance, when user cancels the auth or
         * quits the app.
         */
        void onFailure(int code, String errString);

        /**
         * Called when authentication fails due to non-user related problems such as system errors,
         * phone not having a FP reader etc.
         *
         * @param code  The error code to be returned to Flutter app.
         * @param error The description of the error.
         */
        void onError(String code, String error);

        void onChanged(String code, String error);
    }

    private final FragmentActivity activity;
    private final AuthCompletionHandler completionHandler;
    private final KeyguardManager keyguardManager;
    private final FingerprintManagerCompat fingerprintManager;
    private final MethodCall call;
    private final BiometricPromptV2.PromptInfo promptInfo;
    private final boolean isAuthSticky;
    private final UiThreadExecutor uiThreadExecutor;
    private boolean activityPaused = false;

    @RequiresApi(api = Build.VERSION_CODES.M)
    public AuthenticationHelper(
            FragmentActivity activity, MethodCall call, AuthCompletionHandler completionHandler) {
        this.activity = activity;
        this.completionHandler = completionHandler;
        this.call = call;
        this.keyguardManager = (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
        this.fingerprintManager = FingerprintManagerCompat.from(activity);
        this.isAuthSticky = call.argument("stickyAuth");
        this.uiThreadExecutor = new UiThreadExecutor();
        this.promptInfo =
                new BiometricPromptV2.PromptInfo.Builder()
                        .setDescription((String) call.argument("localizedReason"))
                        .setTitle((String) call.argument("signInTitle"))
                        .setSubtitle((String) call.argument("fingerprintHint"))
                        .setFailHint((String) call.argument("fingerprintNotRecognized"))
                        .setNegativeButtonText((String) call.argument("cancelButton"))
                        .build();
    }

    public void authenticate() {
        if (fingerprintManager.isHardwareDetected()) {
            if (keyguardManager.isKeyguardSecure() && fingerprintManager.hasEnrolledFingerprints()) {
                start();
            } else {
                if (call.argument("useErrorDialogs")) {
                    //showGoToSettingsDialog();
                } else if (!keyguardManager.isKeyguardSecure()) {
                    completionHandler.onError(
                            "PasscodeNotSet",
                            "Phone not secured by PIN, pattern or password, or SIM is currently locked.");
                } else {
                    SharedPreferences sharedPreferences = activity.getSharedPreferences(DEFAULT_KEY_NAME, 0);
                    sharedPreferences.edit().putString(HAS_FINGER_KEY, "").apply();
                    completionHandler.onError("NotEnrolled", "No fingerprint enrolled on this device.");
                }
            }
        } else {
            completionHandler.onError("NotAvailable", "Fingerprint is not available on this device.");
        }
    }

    /**
     * Start the fingerprint listener.
     */
    private void start() {
        activity.getApplication().registerActivityLifecycleCallbacks(this);
        CipherHelper.getInstance().createKey(activity.getBaseContext(), true);
        new BiometricPromptV2(activity, uiThreadExecutor, this).authenticate(promptInfo);
    }

    /**
     * Stops the fingerprint listener.
     */
    private void stop() {
        activity.getApplication().unregisterActivityLifecycleCallbacks(this);
    }

    @SuppressLint("SwitchIntDef")
    @Override
    public void onAuthenticationError(int errorCode, CharSequence errString) {
        switch (errorCode) {
            case BiometricConstants.ERROR_LOCKOUT:
                completionHandler.onError(
                        "LockedOut",
                        "The operation was canceled because the API is locked out due to too many attempts. This occurs after 5 failed attempts, and lasts for 30 seconds.");
                break;
            case BiometricConstants.ERROR_LOCKOUT_PERMANENT:
                completionHandler.onError(
                        "PermanentlyLockedOut",
                        "The operation was canceled because ERROR_LOCKOUT occurred too many times. Biometric authentication is disabled until the user unlocks with strong authentication (PIN/Pattern/Password)");
                break;
            case BiometricConstants.ERROR_CANCELED:
                // If we are doing sticky auth and the activity has been paused,
                // ignore this error. We will start listening again when resumed.
                if (activityPaused && isAuthSticky) {
                    return;
                } else {
                    completionHandler.onFailure(errorCode, errString.toString());
                }
                break;
            default:
                completionHandler.onFailure(errorCode, errString.toString());
        }
        stop();
    }

    @Override
    public void onAuthenticationSucceeded(BiometricPromptV2.AuthenticationResult result) {
        Cipher cipher = result.getCryptoObject().getCipher();
        if (cipher != null) {
            completionHandler.onSuccess();
            stop();
        }

    }

    @Override
    public void onAuthenticationChanged() {
        completionHandler.onChanged("changed", "fingerPringtChanged");
        super.onAuthenticationChanged();
    }

    @Override
    public void onAuthenticationFailed() {
    }

    /**
     * If the activity is paused, we keep track because fingerprint dialog simply returns "User
     * cancelled" when the activity is paused.
     */
    @Override
    public void onActivityPaused(Activity ignored) {
        if (isAuthSticky) {
            activityPaused = true;
        }
    }

    @Override
    public void onActivityResumed(Activity ignored) {
        if (isAuthSticky) {
            activityPaused = false;
            final BiometricPromptV2 prompt = new BiometricPromptV2(activity, uiThreadExecutor, this);
            // When activity is resuming, we cannot show the prompt right away. We need to post it to the
            // UI queue.
            uiThreadExecutor.handler.post(
                    new Runnable() {
                        @Override
                        public void run() {
                            prompt.authenticate(promptInfo);
                        }
                    });
        }
    }


    // Unused methods for activity lifecycle.

    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
    }

    private static class UiThreadExecutor implements Executor {
        public final Handler handler = new Handler(Looper.getMainLooper());

        @Override
        public void execute(Runnable command) {
            handler.post(command);
        }
    }
}
